\bibliographystyle{gbt7714-2005}

\chapter{正则表达式}

\epigraphhead[60]{\epigraph{所谓幸福，就是尽情地施展我们掌握的技能，等待期望的结果。}{\textit{亚里士多德}}}

信息技术日新月异，有些已成昨日黄花，然而伟大的东西却弥久如新，正则表达式（Regular Expression）就是这样一种伟大的发明。它是一个强大、便捷、高效的文本处理工具，能成百倍地提高开发效率和程序质量。正则表达式绝对值得每一个程序员掌握，甚至值得所有知识工作者了解。

然而奇怪的是，这样一个了不起的技术，在我国却没有得到充分推广。究其原因，主要有二：其一，正则表达式产生和发展与UNIX文化体系中，而我国的信息技术教学长期受到微软文化的影响。其二，正则表达式并不是那么容易掌握，需要正确的方法。

学习正则表达式，入门不难，看一些例子，试着模仿，就可粗通。但要真正掌握，还需了解正则表达式的原理，建议阅读Jeffrey Firedl的《精通正则表达式》。这本书自1997年出版后，凭借其质量，成为正则表达式图书领域当之无愧的“圣经”。

\section{正则表达式的发展史}
关于正则表达式，最初的想法来自20世纪40年代的两位神经学家，他们研究出一种模型，描述神经系统在神经元层面上的工作模式。若干年后，数学家Stephen Kleene在代数学中正式描述了这种模型，并将它命名为正则集合。Stephen发明了一套简洁的表明正则集合的方法，他称之为“正则表达式”。\cite{Friedl，2007}

20世纪50年代和60年代，理论数学界对正则表达式进行了充分的研究。

20世纪70年代，开始将正则表达式应用到计算方面，再到后来正则表达式开始应用到其它领域（文本编辑、生物信息、基因图谱等等）。\cite{Friedl，2007}

在UNIX中ed编辑器开始使用正则表达式，其中的一个功能最终发展成独立的工具grep（Global Regular Expression Print）。这个工具在众多程序员的改进下，功能日渐强大。

1986年，Henry Spencer发布了用C语言写的正则表达式包，这是一个具有开创性意义的包，因为它可以毫无困难地移植到其他程序中。

1987年，Larry Well发布了Perl语言的第一个版本，提供了正则表达式操作符，在脚本语言中是首创。之后，Perl语言越来越强壮，错误也越来越少，添加了许多新的正则表达式特性。

其他语言的开发人员借鉴了Perl语言的正则表达式特性，最终在某种程度上“兼容Perl”，如Python、Ruby、Java、PHP、C/C++、.NET都有各自的正则表达式包。

1997年，Philip Hazel开发了PCRE，这是一套兼容Perl正则表达式的库，PCRE的正则引擎质量很高，全面仿制Perl的正则表达式语法和语义。许多流行的开发语言都使用PCRE为用户提供正则功能，如PHP、Apache 2等等。

\section{PHP中的正则函数}
PHP提供了至少3套独立的正则引擎，分别是“preg”、“ereg”和“mb\_ereg"。本书将介绍preg引擎提供的函数，preg使用NFA引擎\footnote{正则引擎主要有两大类，一类是DFA，一类是NFA，其中DFA是文本主导的正则引擎，而NFA是表达式主导的正则引擎。NFA引擎最重要的性质是，它会依次处理各个子表达式，遇到需要在两个可能成功中进行选择的时候，它会选择其一，同时记住另一个，以备后需。}，通常情况下，在速度和功能方面强于其余两者。

下面，我们介绍几个常用的preg函数：

\subsection{preg\_match\_all}
preg\_match\_all函数执行一个全局正则表达式匹配。
\begin{lstlisting}
int preg_match_all ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] )
\end{lstlisting}

搜索subject中所有匹配pattern给定正则表达式的匹配结果，并将它们以flag指定顺序输出到matches中。在第一个匹配找到后，子序列继续从最后一次匹配位置搜索。

其中的主要参数有：
\begin{description}
 	\item [pattern] 要搜索的模式，字符串形式。
 	\item [subject] 输入字符串。
 	\item [matches] 多维数组，作为输出参数输出所有匹配结果, 数组排序通过flags指定。
 \end{description}

preg\_match\_all函数返回完整匹配次数（可能是0），或者如果发生错误返回FALSE。\cite{PHPGroup，2014a}

\subsection{preg\_match}
preg\_match函数执行一个正则表达式匹配。
\begin{lstlisting}
int preg_match ( string $pattern , string $subject [, array &$matches [, int $flags = PREG_PATTERN_ORDER [, int $offset = 0 ]]] )
\end{lstlisting}

搜索subject中所有匹配pattern给定正则表达式的匹配结果，并将它们以flag指定顺序输出到matches中。

其中的主要参数有：
\begin{description}
 	\item [pattern] 要搜索的模式，字符串形式。
 	\item [subject] 输入字符串。
 	\item [matches] 如果提供了参数matches，它将被填充为搜索结果。 \$matches[0]将包含完整模式匹配到的文本， \$matches[1] 将包含第一个捕获子组匹配到的文本，以此类推。
 \end{description}

preg\_match()返回 pattern 的匹配次数。如果发生错误preg\_match()返回 FALSE。  它的值将是0次（不匹配）或1次，因为preg\_match()在第一次匹配后将会停止搜索。preg\_match\_all()不同于此，它会一直搜索subject直到到达结尾。\cite{PHPGroup，2014a}

\subsection{preg\_replace}
preg\_replace函数执行一个正则表达式的搜索和替换。

\begin{lstlisting}
mixed preg_replace ( mixed $pattern , mixed $replacement , mixed $subject [, int $limit = -1 [, int &$count ]] )
\end{lstlisting}

搜索subject中匹配pattern的部分， 以replacement进行替换。

其中的主要参数有：
\begin{description}
  \item [pattern] 要搜索的模式，字符串形式。
  \item [replacement] 用于替换的字符串或字符串数组。
  \item [subject] 要进行搜索和替换的字符串或字符串数组。
 \end{description}

 用法示例：
 \begin{lstlisting}
 <?php
$str = 'foo       o';
$str = preg_replace('/\s\s+/', ' ', $str);
// 将会改变为'foo o'
echo $str;
?>
 \end{lstlisting}

除上述函数外，preg正则函数还有preg\_filter、preg\_grep等等，请读者自行查看\href{http://php.net/manual/zh/book.pcre.php}{PHP参考手册}。

\section{正则表达式的构成要素}
PHP中的正则表达式由四部分组成：定界符、原子、元字符、模式修饰符。

\begin{figure}[!htbp]
	\centering
	\includegraphics[width=60mm]{raw/re.eps}
	\caption{PHP正则表达式的构成要素}
	\label{fig:RegularExpression}
\end{figure}

\begin{description}
	\item [定界符] 定界符可以是任意非字母数字、非反斜线、非空白字符，常用“/”。
	\item [原子] 放在定界符中的最小的一个匹配单位，在一个正则表达式中，至少要有一个原子。正则表达式是一个从左到右匹配目标字符串的模式。
	\item [元字符] 正则表达式的威力源于它可以在模式中拥有选择和重复的能力。一些字符被赋予特殊的涵义，使其不再单纯的代表自己，模式中的这种有特殊涵义的编码字符称为元字符。元字符用来修饰原子。
	\item [模式修饰符] 用来修正正则表达式进行，放在最右边。
\end{description}

\section{原子}
正则表达式中的原子可以是打印字符（键盘上的各种字母,有的需要转义）、非打印字符（如回车、制表符等）、转义符、子表达式（方括弧、圆括弧）。如：
\begin{lstlisting}
<?php
function outputRe($pattern, $string) {
	if (preg_match_all($pattern, $string, $matched)) {
    echo '正则表达式:', htmlentities($pattern), '和字符串：', htmlentities($string), '匹配成功，匹配的字符串是：', "<br />";
    var_dump($matched);
  } else {
    echo "<p>",'没找到匹配字符', "</p>";
  }
}

$pattern = '/php/';
$string =	'12^dff+php+gdjjd<>?/';
outputRe($pattern, $string);

?>
\end{lstlisting}

我们在理解正则表达式的时候，应该按照原子的顺序来解读，上面例子中的正则表达式应该这样理解：
\begin{quote}
% \kaishu
/php/匹配的是以p字符开始，紧接一个h，紧接一个p的文本。
\end{quote}

其他的子表达式（虽然是多个字符构成，但在逻辑上是一个原子），将在后续的内容进行详细讲解。

\section{元字符}
模式中有特殊涵义的编码字符称为元字符。元字符被赋予特殊的涵义，使其不再单纯的代表自己。如:

\begin{quote}
% \kaishu
\textbackslash{d}表示数字，并不表示字母d。
\end{quote}

\subsection{匹配单个字符的元字符}
可以使用元字符匹配指定的单个字符，这是正则表达式的基础，正则表达式的其它功能都建立在对单个字符的匹配之上。
% Table generated by Excel2LaTeX from sheet 'Sheet2'
\begin{table}[htbp]
  \centering
  \caption{匹配单个字符的元字符}
    \begin{tabular}{lll}
    \toprule
            & 元字符     & 匹配对象 \\
    \midrule
    .       & 点号      & 匹配单个任意字符（除换行） \\
    {[……]}   & 字符组     & 匹配单个列出的字符，可以使用连字符"-"表示范围 \\
    {[\^{}……]}  & 排除型字符组  & 匹配单个未列出的字符 \\
    \textbackslash{}char & 转义字符    & 如char是元字符，匹配char对应的普通字符 \\
    \bottomrule
    \end{tabular}%
  \label{tab:匹配单个字符的元字符}%
\end{table}%

字符组（排除型字符组）是子表达式的一种，用方括弧来表示。方括弧[]内的多个字符会被认为是可选的一个字符。例如[123456]匹配1到6中的任意一个数字。[123456]还可以写成[1-6]。

\subsubsection{正则表达式中的特殊字符}
正则表达式特殊字符有：
\begin{lstlisting}
. \ + * ? [ ^ ] $ ( ) { } = ! < > | : -
\end{lstlisting}
当这些字符及定界符需要在模式中进行匹配时，需要在其前面加上反斜线进行转义。可使用php函数preg\_quote()非常方便的进行转义。
\subsection{常用转义符}
% Table generated by Excel2LaTeX from sheet 'Sheet1'
\begin{table}[htbp]
  \centering
  \caption{常用转义符}
    \begin{tabular}{ll}
    \toprule
    \textbackslash{}t & 制表符 \\
    \textbackslash{}n & 换行符 \\
    \textbackslash{}r & 回车符 \\
    \textbackslash{}s & 任何“空白”字符（例如空格符、制表符、进纸符、回车换行符等） \\
    \textbackslash{}S & 除\textbackslash{}s之外的任何字符 \\
    \textbackslash{}w & [a-zA-Z0-9] \\
    \textbackslash{}W & 除\textbackslash{}w之外的任何字符 \\
    \textbackslash{}d & 数字 \\
    \textbackslash{}D & 除\textbackslash{}d外的任何字符 \\
    \bottomrule
    \end{tabular}%
  \label{tab:escape-character}%
\end{table}%

\subsection{提供计数功能的元字符}
元字符还可以用来表示前置字符的数量，尤其是我们不知道字符内容的时候，这个功能就非常有用和高效。
% Table generated by Excel2LaTeX from sheet 'Sheet2'
\begin{table}[htbp]
  \centering
  \caption{提供计数功能的元字符}
    \begin{tabular}{lll}
    \toprule
            & 元字符     & 匹配对象 \\
    \midrule
    ？       & 问号      & 匹配前置字符0-1次 \\
    {*}   & 星号     & 匹配前置字符0-N次 \\
    +  & 加号  & 匹配前置字符1-N次 \\
    \{n,m\} & 区间量词    & 匹配前置字符n-m次 \\
    \bottomrule
    \end{tabular}%
  \label{tab:提供计数功能的元字符}%
\end{table}%

默认情况下，量词都是"贪婪"的，也就是说， 它们会在不导致模式匹配失败的前提下，尽可能多的匹配字符(直到最大允许的匹配次数)。然而，如果一个量词紧跟着一个 ?(问号) 标记，它就会成为懒惰(非贪婪)模式， 它不再尽可能多的匹配，而是尽可能少的匹配。

\subsection{匹配位置的元字符}
元字符还可以用来表示位置，如一行的开始，单词的边界，或者是指定的某个位置。这种表示位置的元字符，又叫断言。一个断言就是一个对当前匹配位置之前或之后的字符的测试， 它不会实际消耗任何字符。
% Table generated by Excel2LaTeX from sheet 'Sheet2'
\begin{table}[htbp]
  \centering
  \caption{匹配位置的元字符}
    \begin{tabular}{lll}
    \toprule
            & 元字符     & 匹配对象 \\
    \midrule
    \^{}       & 脱字符      & 匹配一行的开头位置 \\
    \$   & 美元符     & 匹配一行的结束位置 \\
    \textbackslash{}b & 单词分界符 & 单词的分界位置 \\
    \textbackslash{}B & 单词分界符 & 除单词分界位置的任何位置 \\
    (?<=...) & 环视 & 匹配指定的位置 \\
    \bottomrule
    \end{tabular}%
  \label{tab:匹配位置的元字符}%
\end{table}%
注意在子表达式内部，脱字符\^{}并不表示开头，而是表示“非”。

复杂的断言以子组的方式编码。 它有两种类型： 前瞻断言(匹配到的字符组的前面)和后瞻断言(匹配到的字符组的后面)，每个类型中又分为肯定形式和否定形式。

\subsubsection{前瞻断言}
前瞻断言的肯定形式为(?=……)。
\begin{lstlisting}
$pattern = '/(?=中华人民共和国)/';
// (?=中华人民共和国)匹配中华人民共和国的开始位置
$string  = '中华人民共和国，中华人民共和国，中华民国';
echo preg_replace($pattern, '我爱', $string), "<br />";
\end{lstlisting}
运行结果为：
\begin{lstlisting}
我爱中华人民共和国，我爱中华人民共和国，中华民国
\end{lstlisting}

前瞻断言的否定形式为(?!……)：
\begin{lstlisting}
$pattern = '/(?!中华人民共和国)中华/';
// // (?!中华人民共和国)匹配不是中华人民共和国的开始位置
$string  = '中华人民共和国，中华人民共和国，中华民国';
echo preg_replace($pattern, '我爱中华', $string), "<br />";
\end{lstlisting}
运行结果为：
\begin{lstlisting}
中华人民共和国，中华人民共和国，我爱中华民国
\end{lstlisting}

\subsubsection{后瞻断言}
后瞻断言的肯定形式为(?<=……)：
\begin{lstlisting}
$pattern = '/(?<=中华人民共和国)/';
// (?<=中华人民共和国)匹配中华人民共和国的结束位置
$string  = '中华人民共和国，中华人民共和国，中华民国';
echo preg_replace($pattern, '威武', $string), "<br />";
\end{lstlisting}
运行结果为：
\begin{lstlisting}
中华人民共和国威武，中华人民共和国威武，中华民国
\end{lstlisting}

后瞻断言的否定形式为：(?<!……)
\begin{lstlisting}
$pattern = '/国(?<!中华人民共和国)/';
// // (?<!中华人民共和国)匹配不是中华人民共和国的结束位置
$string  = '中华人民共和国，中华人民共和国，中华民国，美国';
echo preg_replace($pattern, '国人民', $string), "<br />";
\end{lstlisting}
运行结果为：
\begin{lstlisting}
中华人民共和国，中华人民共和国，中华民国人民，美国人民
\end{lstlisting}

\subsubsection{断言的组合}
多个断言可以同时出现。断言子表达式的顺序无前后区分，只要同时满足断言条件的位置，都符合正则表达式模式要求。比如：

\begin{lstlisting}
(?<=\d{3})(?<!999)foo
\end{lstlisting}

匹配前面有三个数字但不是 “999” 的字符串 “foo”。

\subsection{其他元字符}
还有一些元字符用来分割表达式、设定引用方式等。
% Table generated by Excel2LaTeX from sheet 'Sheet2'
\begin{table}[htbp]
  \centering
  \caption{其他元字符}
    \begin{tabular}{lll}
    \toprule
            & 元字符     & 匹配对象 \\
    \midrule
    {|}       & 选择符      & 匹配任意分割的表达式 \\
    {(……)}  & 括号     & 限定多选结构的范围，标注量词的作用范围，为反向引用捕获文本 \\
    /1,/2,……  & 反向引用     & 匹配之前的括号内的子表达式匹配的文本 \\
    \bottomrule
    \end{tabular}%
  \label{tab:其他元字符}%
\end{table}%

竖线字符用于分离模式中的可选路径。 比如模式com|cn匹配 “com”或者“cn”。 竖线可以在模式中出现任意多个，并且允许有空的可选路径(匹配空字符串)。 匹配的处理从左到右尝试每一个可选路径，并且使用第一个成功匹配的。

括号中匹配的内容，可以保存到变量中，命名的规则为(?<name>)：
\begin{lstlisting}
$pattern = '/<b>(?<title>.*?)<\/b>/s';
\end{lstlisting}

\section{模式修饰符}
模式修饰符是对整个正则表达式功能的调整。
\lstinputlisting{example/PHP/08-RE2/04.php}


\section{正则表达式的常见应用}
在写正则表达式时，建议先从简单开始，反复测试，逐渐丰富表达式，直至完全符合要求。由于正则表达式的灵活性，符合某一匹配要求的正则表达式并不止一个。

\subsection{为数字添加千分符}
利用正则表达式，将123456这样的数字变为123,456，如：
\begin{lstlisting}
$pattern = '/(?<=\d)(?=(\d\d\d)+[. ])/';
// $pattern = '/(?<=\d)(?=(\d\d\d)+\b)/';
$string  = 'eeeddfh 1234y45886.56 123445886.56 dfdfdf';
echo preg_replace($pattern, ',', $string), "<br />";
\end{lstlisting}
上面的正则表达式，匹配的是位置，其中：
\begin{lstlisting}
(?=(\d\d\d)+[. ])
\end{lstlisting}
表示的是三位数字之前的位置：
\begin{lstlisting}
(?<=\d)
\end{lstlisting}
表示的某个数字之后的位置。和前面的位置结合在一起，还是表示位置：指的是一个数字后，三个数字前的位置。

\subsection{检验密码}
\label{re4password}
如何让密码必须为包含大写字母、小写字母、数字，同时最少6位？正则表达式如下：
\begin{lstlisting}
$pattern = '/(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)[A-Za-z\d]{6,}/';
\end{lstlisting}
其中最关键的是
\begin{lstlisting}
(?=.*?[a-z])
\end{lstlisting}
它匹配的是小写字母前的任意位置。
\begin{lstlisting}
(?=.*?[a-z])(?=.*?[A-Z])(?=.*?\d)
\end{lstlisting}
表示目标字符串中必须要有字母和数字。

\subsection{检验电子邮件地址}
邮箱地址的正则模式，可以分成@符号前后两大部分：
\begin{lstlisting}
$pattern = '/^\w+[&\-_.]?\w+@\w+([\w\-.]+)?\.\w+/';
\end{lstlisting}

\subsection{匹配HTML中的内容}
\begin{lstlisting}
$pattern = '/<a\s*href="(.*?)".*?>(.*?)<\/a>/s';
$string  = '<a href="/skc/contents/9/526.html" target="_blank">关于召开中央高校基本科研业务费专项资金项目（社会科学类）主持人会议的通知</a></span><span style="float:right;">2014-04-16</span></li><div class="clearfloat"></div><li><span style="float:left;"><img src="style/images/a.jpg" style="margin-right:10px;" /><a href="/skc/contents/9/525.html" target="_blank">关于转发《关于推动中国梦理论研究的通知》的通知</a></span><span style="float:right;">2014-04-10</span></li>';
\end{lstlisting}

% 还需增加正则表达式的其他特征内容，如判断、循环等。

\bibliography{../bib/yangjh}
